package com.miya.demo.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.date.TimeInterval;
import cn.hutool.core.util.ZipUtil;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelWriter;
import com.alibaba.excel.support.ExcelTypeEnum;
import com.alibaba.excel.write.metadata.WriteSheet;
import com.alibaba.excel.write.metadata.WriteTable;
import com.alibaba.excel.write.metadata.WriteWorkbook;
import com.alibaba.excel.write.style.column.LongestMatchColumnWidthStyleStrategy;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.miya.demo.common.util.MinioUtil;
import com.miya.demo.common.util.PojoUtil;
import com.miya.demo.common.util.excel.ExcelUtil;
import com.miya.demo.entity.Customer;
import com.miya.demo.mapper.CustomerMapper;
import com.miya.demo.model.dto.CustomerUploadDTO;
import com.miya.demo.model.vo.CustomerDowloadVO;
import com.miya.demo.model.vo.TemplateExportListVO;
import com.miya.demo.model.vo.TemplateExportVO;
import com.miya.demo.service.CustomerService;
import com.miya.demo.support.listener.easyexcel.CustomerUploadListener;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileItemFactory;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.io.IOUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.commons.CommonsMultipartFile;

import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.CompletableFuture;

/**
 * 客户服务impl
 *
 * @author Caixiaowei
 * @date 2021/10/19
 */
@Service
@Slf4j
public class CustomerServiceImpl extends ServiceImpl<CustomerMapper, Customer> implements CustomerService {

    @Autowired
    private MinioUtil minioUtil;

    @Override
    public void uploadExcel(MultipartFile file) throws IOException {
        EasyExcel.read(file.getInputStream(), CustomerUploadDTO.class, new CustomerUploadListener(this)).sheet().doRead();
    }

    @Override
    public void downloadExcel() {
        // 根据条件查询导出数据
        List<Customer> list = this.list();
        List<CustomerDowloadVO> dowloadVOList = PojoUtil.copyList(list, CustomerDowloadVO.class);
        ExcelUtil.export(dowloadVOList, CustomerDowloadVO.class, "客户数据");
    }

    @Override
    public void downloadExcel2(HttpServletResponse response) {
        exportBigData2(response);
    }

    /**
     * excel模板导出下载
     */
    @Override
    public void downloadExcelOfTemplate(HttpServletResponse response) {
        // 查询要导出的数据
        TemplateExportVO exportVO = TemplateExportVO.builder()
                .shopName("corki 之家")
                .shopAdd("北京路1010")
                .statisticsDate(new Date())
                .build();

        TemplateExportListVO exportListVO1 = TemplateExportListVO.builder()
                .name("corki")
                .age(33)
                .gender("男")
                .phone("13300000000")
                .employmentDate(new Date())
                .build();
        TemplateExportListVO exportListVO2 = TemplateExportListVO.builder()
                .name("miya")
                .age(18)
                .gender("女")
                .phone("18300000000")
                .employmentDate(new Date())
                .build();
        Map<String, Object> map1 = BeanUtil.beanToMap(exportListVO1);
        Map<String, Object> map2 = BeanUtil.beanToMap(exportListVO2);
        ArrayList<Map<String, Object>> list = CollUtil.newArrayList(map1, map2);
        exportVO.setClerkTotal(list.size());
        Map<String, Object> map = BeanUtil.beanToMap(exportVO);
        String templateFilePath = "excel".concat(File.separator).concat("shopStatistics.xlsx");
        ExcelUtil.exportByTemplate(map, list, response, templateFilePath, "shopStatistics.xlsx");

    }

    /**
     * 大数据量导出: 导出到一个excel文件, 多个sheet
     *  1. 根据数据量多少 && 每个sheet记录数 计算sheet的数量
     *  2. 约定每次查询db的记录数, 计算每个sheet要查询db 的次数
     *  3. 每次从数据库读取处理后清空集合, 方便提前被GC回收, 防止内存占用过大
     *
     * @param response 响应
     */
    public void exportBigData(HttpServletResponse response) {
        try (OutputStream outputStream = response.getOutputStream()) {
            TimeInterval ti = new TimeInterval();
            long startTime = System.currentTimeMillis();
            log.info("导出开始时间:{}", startTime);
            WriteWorkbook writeWorkbook = new WriteWorkbook();
            writeWorkbook.setOutputStream(outputStream);
            writeWorkbook.setExcelType(ExcelTypeEnum.XLSX);
            ExcelWriter writer = new ExcelWriter(writeWorkbook);
            String fileName = new String(("export-excel").getBytes(), StandardCharsets.UTF_8);
            // TODO WriteTable 标题这块可以作为公共的封装起来：通过反射获取变量上注解等
            WriteTable table = new WriteTable();
            List<List<String>> titles = new ArrayList<List<String>>();
            titles.add(Collections.singletonList("name"));
            titles.add(Collections.singletonList("email"));
            table.setHead(titles);
            // 记录总数：实际中需要根据查询条件（过滤数据）进行统计即可，
            // 此处写入限定的条数进行自测
            Long totalCount = this.count();
            // 每一个Sheet存放100w条数据
            Long sheetDataRows = ExcelUtil.PER_SHEET_ROW_COUNT;
            // 每次写入的数据量20w
            Long writeDataRows = ExcelUtil.PER_WRITE_ROW_COUNT;
            // 计算需要的Sheet数量
            Long sheetNum = totalCount % sheetDataRows == 0L ? (totalCount / sheetDataRows) : (totalCount / sheetDataRows + 1);
            // 计算一般情况下每一个Sheet需要写入的次数(一般情况不包含最后一个sheet,因为最后一个sheet不确定会写入多少条数据)
            Long oneSheetWriteCount = totalCount > sheetDataRows ? sheetDataRows / writeDataRows : totalCount % writeDataRows > 0L ? totalCount / writeDataRows + 1 : totalCount / writeDataRows;
            // 计算最后一个sheet需要写入的次数
            Long lastSheetWriteCount = totalCount % sheetDataRows == 0 ? oneSheetWriteCount : (totalCount % sheetDataRows % writeDataRows == 0L ? (totalCount / sheetDataRows / writeDataRows) : (totalCount / sheetDataRows / writeDataRows + 1));

            // 开始分批查询分次写入
            // 注意这次的循环就需要进行嵌套循环了,外层循环是Sheet数目,内层循环是写入次数
            List<List<String>> dataList = new ArrayList<>();
            for (int i = 0; i < sheetNum; i++) {
                //创建Sheet
                WriteSheet sheet = new WriteSheet();
                sheet.setSheetNo(i);
                sheet.setSheetName(fileName + i);
                // 循环写入次数: j的自增条件是当不是最后一个Sheet的时候写入次数为正常的每个Sheet写入的次数,如果是最后一个就需要使用计算的次数lastSheetWriteCount
                for (int j = 0; j < (i != sheetNum - 1 || i == 0 ? oneSheetWriteCount : lastSheetWriteCount); j++) {
                    // 集合复用,便于GC清理
                    dataList.clear();
                    // 分页查询一次20w
                    Page<Customer> page = this.page(new Page<>(j + 1 + oneSheetWriteCount * i, writeDataRows));
                    List<Customer> records = page.getRecords();
                    if (CollUtil.isNotEmpty(records)) {
                        records.forEach(item -> {
                            dataList.add(Arrays.asList(item.getName(), item.getEmail()));
                        });
                    }
                    // 写数据
                    writer.write(dataList, sheet, table);
                }
            }

            // 下载EXCEL 以下代码可以作为公共的进行封装.
            setExcelRespProp(response, fileName);
            writer.finish();
            outputStream.flush();
            // 导出时间结束
            long interval = ti.interval();
            log.info("导出所用时间:{}", interval + "秒");


        } catch (Exception e) {

        }

    }

    public void exportBigData2(HttpServletResponse response) {
        log.info("*********查询列表导出开始！**************");
        //文件名
        String fileName =String.valueOf(System.currentTimeMillis());

        try (OutputStream outputStream = response.getOutputStream()) {
            //记录总数:实际中需要根据查询条件进行统计即可:一共多少条
            Long totalCount= this.count();
            // 每一个Sheet存放100w条数据
            Long sheetDataRows = ExcelUtil.PER_SHEET_ROW_COUNT;
            // 每次写入的数据量20w
            Long writeDataRows = ExcelUtil.PER_WRITE_ROW_COUNT;
            // 计算需要的Sheet数量
            Long sheetNum = totalCount % sheetDataRows == 0L ? (totalCount / sheetDataRows) : (totalCount / sheetDataRows + 1);
            // 计算一般情况下每一个Sheet需要写入的次数(一般情况不包含最后一个sheet,因为最后一个sheet不确定会写入多少条数据)
            Long oneSheetWriteCount = totalCount > sheetDataRows ? sheetDataRows / writeDataRows : totalCount % writeDataRows > 0L ? totalCount / writeDataRows + 1 : totalCount / writeDataRows;
            // 计算最后一个sheet需要写入的次数
            Long lastSheetWriteCount = totalCount % sheetDataRows == 0 ? oneSheetWriteCount : (totalCount % sheetDataRows % writeDataRows == 0L ? (totalCount / sheetDataRows / writeDataRows) : (totalCount / sheetDataRows / writeDataRows + 1));

            //必须放到循环外，否则会刷新流
            ExcelWriter excelWriter = EasyExcel.write(outputStream).build();
            //开始分批查询分次写入
            for (int i = 0; i < sheetNum; i++) {
                //创建Sheet
                WriteSheet sheet = new WriteSheet();
                sheet.setSheetName("Sheet"+i);
                sheet.setSheetNo(i);
                //循环写入次数: j的自增条件是当不是最后一个Sheet的时候写入次数为正常的每个Sheet写入的次数,如果是最后一个就需要使用计算的次数lastSheetWriteCount
                for (int j = 0; j < (i != sheetNum - 1 ? oneSheetWriteCount : lastSheetWriteCount); j++) {
                    //分页查询一次20w
                    Page page = new Page(j + 1 + oneSheetWriteCount * i, writeDataRows);
                    //查询分页列表---按照自己的业务查列表，分页这个一定要使用这个：page1.getPageNum(),page1.getPageSize()！！！
                    Page page1 = this.page(page);
                    List<Customer> records = page1.getRecords();
                    List<CustomerDowloadVO> list = PojoUtil.copyList(records, CustomerDowloadVO.class);

                    //写入到excel:
                    /**************z只需要选择一种方式即可*****************/
                    //这里可以通过设置includeColumnFiledNames、excludeColumnFiledNames导出什么字段，可以动态配置，前端传过来那些列，就导出那些列
                    WriteSheet writeSheet = EasyExcel.writerSheet(i, "Sheet" + (i + 1)).head(CustomerDowloadVO.class)
                            .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()).build();
                    excelWriter.write(list, writeSheet);
                }
            }
            // 下载EXCEL，返回给前段stream流
            response.setContentType("application/octet-stream");
            response.setCharacterEncoding("utf-8");
            response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
            excelWriter.finish();
            outputStream.flush();
            outputStream.close();
            log.info("*********查询列表导出结束！**************");
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

    /**
     * 大数据量导出: 异步
     *  1. 数据分批查询, 生成多个excel
     *  2. excel文件打包zip, 上传第三方oss
     *  3. 返回oss的下载url
     *
     */
    @Override
    public String exportBigData3() {
        log.info("********* 异步多线程大数据量异步导出begin **************");
        String fileName = "客户" + DateUtil.format(new Date(), DatePattern.PURE_DATETIME_PATTERN) + ".zip";
        String url = minioUtil.getObjectUrl(MinioUtil.DEFAULT_BUCKET, fileName);

        CompletableFuture.runAsync(()->{
            uploadExportData(fileName);
        });
        return url;
    }

    private void uploadExportData(String fileName) {
        File zipFile = null;
        try {
            //excel文件输入流
            InputStream[] ins = new InputStream[1];
            //excel文件名称路径
            String[] paths = new String[1];
            //查询记录数量,拆分多个excel处理
            Long totalCount = this.count();
            Long excelSize = 5L;
            Long excelCount = totalCount % excelSize == 0L ? (totalCount / excelSize) : (totalCount / excelSize + 1);
            // 每个excel写入的数据
            List<CustomerDowloadVO> list = CollUtil.newArrayList();
            if (excelCount == 0) {
                List<Customer> customerList = this.list();
                list = PojoUtil.copyList(customerList, CustomerDowloadVO.class);

                ByteArrayOutputStream out = new ByteArrayOutputStream();
                EasyExcel.write(out, CustomerDowloadVO.class).sheet().doWrite(list);
                ins[0] = new ByteArrayInputStream(out.toByteArray());
                paths[0] = "客户" + DateUtil.format(new Date(), DatePattern.PURE_DATETIME_PATTERN) + ".xlsx";
            } else {
                ins = new InputStream[excelCount.intValue()];
                paths = new String[excelCount.intValue()];
                for (int i = 1; i <= excelCount; i++) {
                    ByteArrayOutputStream out = new ByteArrayOutputStream();

                    Page page = new Page(i, excelSize);
                    List records = this.page(page).getRecords();
                    list = PojoUtil.copyList(records, CustomerDowloadVO.class);

                    EasyExcel.write(out, CustomerDowloadVO.class).sheet().doWrite(list);
                    ins[i - 1] = new ByteArrayInputStream(out.toByteArray());
                    paths[i - 1] = "客户" + DateUtil.format(new Date(), DatePattern.PURE_DATETIME_PATTERN) + "_" + i + ".xlsx";
                }
            }

            // zip文件
            zipFile = ZipUtil.zip(new File(fileName), paths, ins);

            // 封装MultipartFile对象
            MultipartFile multipartFile = new CommonsMultipartFile(createFileItem(new FileInputStream(zipFile), fileName));
            // minio/阿里云等第三方
            String putObject = minioUtil.putObject(MinioUtil.DEFAULT_BUCKET, fileName, multipartFile.getInputStream());
            log.info("uploadExportData--->putObject={}", putObject);
        } catch (Exception e) {
            log.error("fileUpload occur e:{}", e);
        } finally {
            if (null != zipFile) {
                // 删除临时文件
                zipFile.delete();
            }
        }
    }

    private static FileItem createFileItem(InputStream is, String fileName) throws IOException {
        FileItemFactory factory = new DiskFileItemFactory(16, null);
        FileItem fileItem = factory.createItem("textField", "application/zip", true, fileName);

        OutputStream fileItemOutStream = fileItem.getOutputStream();

        IOUtils.copy(is, fileItemOutStream);

        return fileItem;
    }


    /**
     * 将excel 响应头
     *
     * @param response    响应
     * @param rawFileName 原始文件名称
     */
    public static void setExcelRespProp(HttpServletResponse response, String rawFileName) {
        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
        response.setCharacterEncoding("utf-8");
        String fileName = null;
        try {
            fileName = URLEncoder.encode(rawFileName, "UTF-8").replaceAll("\\+", "%20");
        } catch (UnsupportedEncodingException e) {
            log.error("设置excel下载响应头属性，失败 {}", e.getMessage());
        }
        response.setHeader("Content-disposition", "attachment;filename=utf-8''" + fileName + ".xlsx");
    }
}
